Norikra+FluentdでDoS攻撃をブロックする仕組みを作ってみた
Norikraとは
Norikraとはリアルタイム集計プロダクトです。イベントストリームに対してSQLライクな言語で処理を書くことが出来ます。
例えば、ApacheのアクセスログをNorikraに流し込み、1分あたりのアクセス数やレスポンスタイムの最大値をリアルタイムに集計することが出来ます。
Norikraの利用例は作者であるtagomorisさんのブログで紹介があります。
今回は、Norikraを使ってDoS攻撃をブロックする仕組みを作ってみました。
DoS攻撃ブロックの仕組み
アクセス元はApacheのアクセスログから取得し、ログの受け渡しにはFluentdを利用しました。
ブロックの手順は以下のようになります。
- アクセスログをFluentdのin_tailプラグインで取得。
- Fluentdのout_norikraプラグインで、アクセスログをNorikraに流し込み。
-
Norikraのクエリで、単位時間あたりのアクセス数が多いIPアドレスを抽出。
結果をin_norikraプラグインでFluentdに出力。 -
IPアドレスのリストをout_exec_filterプラグインで、独自スクリプトに渡す。
独自スクリプト内の処理で、AWSのNetwork ACLの設定を変更し、問題のIPアドレスからのinboundを拒否する。
ブロック結果はそのまま、Fluentdに戻される。 -
out_snsプラグインで、ブロック結果をSNSのトピックに出力。
トピックのサブスクライバであるメールアドレス等に通知が飛ぶ。
上図の③にあるSQL風のクエリに注目してください。
重要なのはFROM句にある「m分ごと」の部分で、ここで指定した期間のログに対してクエリを実行することが出来ます。
これを使って、例えば「1分間で1000回以上アクセスのあるIPアドレス」といった抽出が可能になります。
では、実際に構築していきます。
前提
VPC内のEC2上に構築していきます。
AWSリソースへのアクセスが必要なので、IAM Role付きで立ち上げてください。
AMI | Amazon Linux AMI 2014.03 |
セキュリティグループ | 22/TCP, 80/TCP, 26578/TCPを許可 |
IAM Role | Power User Access |
作業は基本的にec2-userで行います。
Norikraのインストール
NorikraはJRubyで動いているので、まずJrubyをインストールします。 *1
まず、rbenvをインストールし、
$ sudo yum install -y git gcc-c++ $ git clone https://github.com/sstephenson/rbenv.git ~/.rbenv $ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile $ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile $ exec $SHELL -l $ rbenv --version rbenv 0.4.0-95-gf71e227
バージョンが確認できたら、JRubyをインストールします。
$ git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build $ rbenv install -l|grep jruby jruby-1.5.6 (snip) jruby-1.7.9 jruby-9000-dev jruby-9000+graal-dev $ rbenv install jruby-1.7.9 $ rbenv shell jruby-1.7.9 $ ruby -v jruby 1.7.9 (1.9.3p392) 2013-12-06 87b108a on OpenJDK 64-Bit Server VM 1.7.0_51-mockbuild_2014_03_13_04_35-b00 [linux-amd64]
現時点の最新版、1.7.9をインストールしました。
普段使うRubyがJRubyになってしまうと辛いので、rbenv shellでログインシェルのみJRubyを利用するようにしています。
GemでNorikraをインストールします。
$ gem install norikra --no-ri --no-rdoc $ rbenv rehash $ which norikra ~/.rbenv/shims/norikra $ gem list --local | grep norikra norikra (0.1.5 java) norikra-client-jruby (0.1.5 java)
Norikraがインストールできました。
Norikraの起動と動作確認
起動する前に、ログや設定ファイルを設置するディレクトリを作成します。
$ sudo mkdir /{etc,var/log,var/run}/norikra $ sudo chown ec2-user:ec2-user /{etc,var/log,var/run}/norikra/
起動時に設定できるパラメータはいろいろあるのですが、とりあえず最低限のパラメータで起動してみましょう。
$ norikra start --stats=/etc/norikra/norikra.json -l /var/log/norikra --daemonize
パラメータの詳しい説明は公式ドキュメントを参照してください。
起動が確認できたら、NorikraのウェブUIにアクセスしてみましょう。
http://hostname:26578/
でアクセスできます。
Norikraが利用するメモリや、現在受付中のターゲット、設定済みのクエリ、処理されたイベント数が確認できます。
クエリの設定もこのUIから出来ます。
ウェブUIへのアクセスが確認できたところで、早速ターゲットを作ってみましょう。
ターゲットとはRDBで言うところのテーブルのようなものです。
$ norikra-client target open test
これで、「test」という名前のターゲットが作られました。
ウェブUIで確認してみましょう。
「Fields」項目が「lazy target」となっています。フィールドとはRDBで言うところのカラムのようなものです。ターゲット作成時にフィールドタイプを指定することも出来ますが、指定しなくても、イベント登録時に自動で判別してくれます。
では、testターゲットにイベントを流してみましょう。
$ echo '{"name":"spike", "age":27}' | norikra-client event send test
ウェブUIで確認すると、フィールドタイプが確定したことが確認できます。
ただ、クエリを登録していないため、いま流したイベントは処理されることなく流れてしまってます。
ウェブUIからクエリを登録してみましょう。
最初なので、全イベントを取得するクエリを「get_all」という名前で登録してみます。
「SELECT フィールド名 FROM ターゲット名」と、SQLと同じようにかけます。
フィールド名にはワイルドカードも使えます。
クエリを登録した状態で、イベントを流してみましょう。
$ echo '{"name":"jet", "age":36}' | norikra-client event send test $ echo '{"name":"faye", "age":77}' | norikra-client event send test
ウェブUIで確認すると、今度はイベントがget_allクエリで処理されたことが確認できます。
最初に流したイベントが出力されていないことに注意してください。
Norikraはデータベースではないため、受け取ったイベントを保存することはありません。
クエリに当てはまるイベントが処理され、その処理結果が出力されます。
Norikraクライアントで結果を取得しましょう。
$ norikra-client event fetch get_all {"time":"2014/03/31 05:40:58","name":"jet","age":36} {"time":"2014/03/31 05:44:09","name":"faye","age":77}
Norikraクライアントの使い方は公式ドキュメントを参照してください。
続いて、FluentdからNorikraにイベントを流すようにしましょう。
Fluentdのインストールと設定
Fluentdの前に、ログを出力するApacheをインストール、設定しておきます。
$ sudo yum install -y httpd24 $ echo ok | sudo tee /var/www/html/index.html $ sudo service httpd start $ sudo chkconfig httpd on $ curl localhost ok
Fluentdと各種プラグインをインストールします。
$ curl -L http://toolbelt.treasuredata.com/sh/install-redhat.sh | sh $ sudo /usr/lib64/fluent/ruby/bin/fluent-gem install fluent-plugin-norikra --no-ri --no-rdoc
Fluentdの設定ファイルを編集し、ApacheのアクセスログをNorikraに流し込むようにします。
最初に説明した手順①、②の部分の設定です。
$ sudo chmod a+rx /var/log/httpd $ sudo mkdir /etc/td-agent/pos $ sudo chown td-agent:td-agent /etc/td-agent/pos $ cat /etc/td-agent/td-agent.conf <source> type tail format /^(?<host>[^ ]*) [^ ]* (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^ ]*) +\S*)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")?$/ time_format %d/%b/%Y:%H:%M:%S %z path /var/log/httpd/access_log tag apache.access pos_file /etc/td-agent/pos/apache.access_log </source> <match apache.access> type norikra norikra localhost:26571 buffer_queue_limit 1 retry_limit 0 remove_tag_prefix apache target_map_tag true </match>
最初に、Fluentdのin_tailプラグイン用にディレクトリの作成や権限変更を行っています。
Fluentd設定の後半で、Norikraへのログの流し込みを設定しています。Fluetndのタグ名がNorikraのターゲット名になるようにしつつ、タグプレフィックス「apache」を削除するようにしているので、「access」というターゲットがNorikraに作られるようになるはずです。
これで、アクセスログをNorikraに流し込む準備ができたので、100回くらいアクセスして動作を見てみましょう。
$ ab -c 10 -n 100 http://`curl ifconfig.me`/index.html
ウェブUIを見ると、新たに「access」というターゲットが出来ていることが確認できます。
IPアドレス毎の1分間のアクセス数を取得するクエリを追加してみます。
SELECT host, COUNT(*) FROM access.win:time_batch(1 min) GROUP BY host
というクエリを登録しています。
ほぼSQLの集約クエリと同じですが、ターゲット名「access」に続くデータウィンドウ「.win:time_batch(1 min)」の部分で、1分毎の集計を行うように指定しています。
データウィンドウの一覧はEsperのドキュメントで確認できます。EsperとはNorikraが利用しているイベント処理エンジンです。
クエリの登録がすんだら、何回かアクセスして、結果を見てみましょう。
$ date Tue Apr 1 05:33:24 UTC 2014 $ ab -c 10 -n 100 http://`curl ifconfig.me`/index.html $ date Tue Apr 1 05:34:44 UTC 2014 $ ab -c 10 -n 200 http://`curl ifconfig.me`/index.html
最初に100アクセス、1分後に200アクセスしてみました。出力を確認するとと、
$ norikra-client event fetch access_count_per_1min {"time":"2014/04/01 05:34:01","host":"54.199.254.150","count(*)":100} {"time":"2014/04/01 05:35:01","host":"54.199.254.150","count(*)":200} {"time":"2014/04/01 05:36:01","host":"54.199.254.150","count(*)":0}
期待通りの結果が得られました。
DoS攻撃検知用クエリの設定
それでは、手順③、NorikraでDoS攻撃を検知する設定に進みます。
今回は「同じIPアドレスから1分間に1,000リクエストあった場合」攻撃と見なすようしました。
Norikraのクエリは次のようになります。
block.access_over_1000_per_1min
SELECT host, COUNT(*) as requests FROM access.win:time_batch(1 min) GROUP BY host HAVING COUNT(*) >= 1000
最初に500アクセス、少し時間をおいて1000アクセスしてみます。
$ date Tue Apr 1 05:59:16 UTC 2014 $ ab -c 100 -n 500 http://`curl ifconfig.me`/index.html $ date Tue Apr 1 06:01:22 UTC 2014 $ ab -c 100 -n 1000 http://`curl ifconfig.me`/index.html
結果を取得してみましょう。
$ norikra-client event fetch block.access_over_1000_per_1min {"time":"2014/04/01 06:02:19","requests":1000,"host":"54.199.254.150"}
ちゃんと1000アクセスした場合のみ取得できています。念のため、アクセス数で絞り込みしてないクエリの結果も見てみましょう。
$ norikra-client event fetch access_count_per_1min {"time":"2014/04/01 06:00:01","host":"54.199.254.150","count(*)":500} {"time":"2014/04/01 06:01:01","host":"54.199.254.150","count(*)":0} {"time":"2014/04/01 06:02:01","host":"::1","count(*)":1} {"time":"2014/04/01 06:02:01","host":"54.199.254.150","count(*)":1000} {"time":"2014/04/01 06:03:01","host":"::1","count(*)":0} {"time":"2014/04/01 06:03:01","host":"54.199.254.150","count(*)":0}
こちらでは500リクエストの場合も取得できています。期待通りです。
DoS攻撃ブロック設定
DoS攻撃を検知できるようになったので、次に検知されたIPアドレスをブロックする仕組みを作ります。手順④の部分です。
まず、Fluentdの設定に下記ディレクティブを追記し、Norikraから結果を取得し、その結果を外部スクリプトに渡すようにします。
<source> type norikra norikra localhost:26571 <fetch> method sweep tag query_name tag_prefix norikra.query interval 60s </fetch> </source> <match norikra.query.block.*> type exec_filter command /usr/local/bin/blocker ec2.ap-northeast-1.amazonaws.com acl-77527b1f block in_format json out_format msgpack tag access.blocked </match>
前半のsourceディレクティブはNorikraから結果を取得する部分です。60秒に1回、全結果を取得し、「norikra.query」というタグプレフィックスがつくようにしています。タグにはNorikraのクエリ名もつくので、実際のタグは「norikra.query.count_access」や「norikra.query.block.access_over_1000_per_1min」の様になります。
後半のディレクティブはNorikraから受け取った結果イベントを外部スクリプトに渡す部分です。
「/usr/local/bin/blocker」というスクリプト内の処理でAWSのNetwork ACLの設定を変更し、受け取ったIPドレスからの接続を拒否するようにしています。引数として、
- 第1引数(必須):EC2のエンドポイント
- 第2引数(必須):Network ACLのID
- 第3引数(任意):ブロックフラグ(ブロックする場合「block」と指定)
-
を取ります。
外部スクリプトからの結果には、「access.blocked」というタグがつけられます。
外部スクリプトの内容は次のようになります。
/usr/local/bin/blocker
#!/usr/lib64/fluent/ruby/bin/ruby require 'aws-sdk' require 'json' require 'msgpack' $stdout.sync = true endpoint = ARGV[0] acl_id = ARGV[1] block = ARGV[2] == 'block' ec2 = AWS::EC2.new(ec2_endpoint: endpoint) acl = ec2.network_acls[acl_id] while input = STDIN.gets begin attacker = JSON.parse(input) rescue next end if block begin allow_any_rule = acl.entries. select {|r| r.ingress && r.action == :allow && r.protocol == -1 && r.cidr_block == "0.0.0.0/0" }. sort_by {|r| r.rule_number}. first deny_rules = acl.entries. select {|r| r.ingress && r.action == :deny && r.rule_number < allow_any_rule.rule_number }. sort_by {|r| r.rule_number} next_rule_number = deny_rules.empty? ? 1 : deny_rules.last.rule_number + 1 unless deny_rules.any? {|r| r.cidr_block == "#{attacker['host']}/32"} acl.create_entry( rule_number: next_rule_number, action: 'deny', protocol: -1, cidr_block: "#{attacker['host']}/32", egress: false, ) end attacker[:status] = 'blocked' rescue attacker[:status] = 'failed to block' end else attacker[:status] = 'detected' end attacker[:proccessed_at] = Time.now.to_s print MessagePack.pack(attacker) end
実行権限を付与しておきます。
$ sudo chmod a+x /usr/local/bin/blocker
AWSのNetwork ACLはマネジメントコンソールから確認できます。
現在は、どこからのアクセスも許可している状態です。
では、実際、ブロックされるか確認してみます。
設定を反映させるため、Fluentdを再起動し、
$ sudo service td-agent restart
1000回アクセスしてみます。
$ curl ifconfig.me 54.199.254.150 $ ab -c 100 -n 1000 http://`curl ifconfig.me`/index.html
1分ほどたってから、マネジメントコンソールを再読み込みしてみると、アクセス元である「54.199.254.150」がブロックされているのが確認できます。
Network ACLのルールはルール番号の小さい方から適応されます。ですので、全許可ルール(100番)の1つ手前まで99個の拒否ルールを作ることが可能です。
より多くのIPアドレスを拒否したい場合は、全許可ルールの番号を大きくするとよいでしょう。
念のため、cURLコマンドでアクセスできるか試してみます。
$ curl http://`curl ifconfig.me`/index.html % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 15 0 15 0 0 280 0 --:--:-- --:--:-- --:--:-- 283 curl: (7) Failed to connect to 54.199.254.150 port 80: Connection timed out
ちゃんとブロックされてますね。
ブロック結果の通知設定
最後に、ブロックの結果をSNSで通知するようにしましょう。手順の⑤にあたる部分です。
Fluentdのプラグインをインストールします。
$ sudo /usr/lib64/fluent/ruby/bin/fluent-gem install fluent-plugin-sns --no-ri --no-rdoc
Fluentdの設定ファイルにSNS用のディレクティブを追加します。
あらかじめSNSトピックを作り自分のメールアドレスをサブスクライバに登録しておいてください。
/etc/td-agent/td-agent.conf
<match access.blocked> type sns sns_topic_name test sns_endpoint sns.ap-northeast-1.amazonaws.com sns_subject block_host #constant subject </match>
「test」という名前のSNSトピックに「block_host」というタイトルで通知が飛ぶようにしました。
先ほど、Network ACLに追加されたDenyルールを削除し、Fluentdを再起動した後、もう一度動作確認します。
$ ab -c 100 -n 1000 http://`curl ifconfig.me`/index.html
しばらくすると、SNSのサブスクライバに登録したメールアドレス宛に次のようなメールが届きました。
{"requests":1000,"host":"54.199.254.150","status":"blocked","proccessed_at":"2014-04-01 08:15:50 +0000","time":"2014-04-01 08:15:50 +0000"} -- If you wish to stop receiving notifications from this topic, please click or visit the link below to unsubscribe: https://sns.ap-northeast-1.amazonaws.com/unsubscribe.html?SubscriptionArn=xxxxxxxxxx Please do not reply directly to this e-mail. If you have any questions or comments regarding this email, please contact us at sns-question@amazon.com
Fluentdの設定ファイルをまとめると次のようになります。
<source> type tail format /^(?<host>[^ ]*) [^ ]* (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^ ]*) +\S*)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")?$/ time_format %d/%b/%Y:%H:%M:%S %z path /var/log/httpd/access_log tag apache.access pos_file /etc/td-agent/pos/apache.access_log </source> <match apache.access> type norikra norikra localhost:26571 buffer_queue_limit 1 retry_limit 0 remove_tag_prefix apache target_map_tag true </match> <source> type norikra norikra localhost:26571 <fetch> method sweep tag query_name tag_prefix norikra.query interval 60s </fetch> </source> <match norikra.query.block.*> type exec_filter command /usr/local/bin/blocker ec2.ap-northeast-1.amazonaws.com <<Network ACL ID>> block in_format json out_format msgpack tag access.blocked </match> <match access.blocked> type sns sns_topic_name <<SNS Topic Name>> sns_endpoint sns.ap-northeast-1.amazonaws.com sns_subject block_host #constant subject </match> <match norikra.query.detect.*> type exec_filter command /usr/local/bin/blocker ec2.ap-northeast-1.amazonaws.com <<Network ACL ID>> in_format json out_format msgpack tag access.detected </match> <match access.detected> type sns sns_topic_name <<SNS Topic Name>> sns_endpoint sns.ap-northeast-1.amazonaws.com sns_subject detect_host #constant subject </match>
まとめ
DoS攻撃を検知し、ブロックする仕組みを作ってみました。
Norikuraを使うのは今回が初めてですが、簡単にクエリを追加、削除できるのが楽しいですね。
SQLと似ているので、何となくで書いても大体動きます。
フィールドタイプの判別も自動でやってくれるので、フィールドが増減した場合も特に対応は必要無さそうです。
例えば、ブルートフォース攻撃を防ぎたいと思った場合、
- ログイン失敗をアクセスログに出力するようにする
- Fluentdのin_tailディレクティブのformatを修正
- Norikraでクエリ追加
だけで対応できます。
クエリはこんな感じでしょうか。
block.login_failed_over_60_per_1min
SELECT host, COUNT(*) AS login_failed FROM access.win:time_batch(1 min) WHERE auth = 'failed' GROUP BY host HAVING COUNT(*) >= 60
Norikraのインストールは10分もあればできるので、皆さんもぜひ使ってみてください。